ΦFlow Animation Gallery¶

GitHub   •   Documentation   •   API   •   Demos

Google Collab Book

This notebook shows various animations created with ΦFlow. To animate a plot, simply pass sequence data to vis.plot() and specify the time dimension using the animate argument.

All animations are rendered with Matplotlib and ffmpeg.

In [1]:
# !pip install --quiet phiflow
In [2]:
from phi.flow import *
np.seterr(all="ignore");

Julia Set¶

The Julia set for the function f(z) = z² + c where we animate c = 0.7885 exp(2πi t) over time. A point z' belongs to the Julia set if the sequence obtained by iterating f(z) starting with z' does not diverge. In the below animation, the plot shows the number of iterations until the sequence reaches |z| ≥ 2. For a more detailed explanation, see the related video showcase of the Mandelbrot set.

In [3]:
def julia(re, im, a=math.linspace(0, 2*PI, batch(t=100))):
    return iterate(lambda z, k: (z ** 2 + 0.7885 * math.exp(1j*a), k + (abs(z)<2)), 50, re + im*1j, 0)[1]
plot(CenteredGrid(julia, re=256, im=256, bounds=Box(re=(-2, 2), im=(-2, 2))), animate='t')
Out[3]:
Your browser does not support the video tag.

Line Plots¶

We define the sine waves $\sin(x - t)$ and $\sin(x + t)$ and sample them on a grid from $x=0$ to $x=2\pi$ with resolution $R_x = 100$. This is done for 60 values of $t$, linearly spaced between $0$ and $4\pi$. These curves are animated in the left plot and their sum, a standing wave, is plotted on the right.

In [4]:
curves = CenteredGrid(lambda x, t: stack([math.sin(x - t), math.cos(x + t)], channel('c')), x=100, t=60, bounds=Box(x=2*PI, t=4*PI)).t[:-1]
plot({"Curves": curves, "Sum": sum(curves.c)}, animate='t')
Out[4]:
Your browser does not support the video tag.

Geometric Primitives¶

Geometric primitives like Sphere and Box can be plotted directly. Instance dimensions denote collections of objects.

In [5]:
plot(Sphere(x=wrap([0, 2], instance('s')), y=0, radius=math.linspace(0, 1, batch(t=50))**.5), animate='t')
Out[5]:
Your browser does not support the video tag.
In [6]:
x = math.range(instance(boxes=10))
plot(Box(x=(x, x+1), y=(0, 2 * math.sin(math.linspace(0, 2*PI, batch(t=30)) + x*.5))), animate='t')
Out[6]:
Your browser does not support the video tag.

Quiver Plots¶

In addition to the point locations, PointClouds can store per-point values, such as vectors.

In [7]:
grid = flatten(CenteredGrid(x=12, y=10).elements).center
direction = math.rotate_vector(vec(x=0, y=1), angle=math.linspace(0, 2*PI, batch(time=50)))
plot(PointCloud(grid, direction), animate='time')
Out[7]:
Your browser does not support the video tag.

2D Scalar Noise¶

Here we visualize the built-in class Noise, sampling it on a $64^3$ grid ranging from 0 to 10 along each axis. We plot all $x$-$y$ slices over time, yielding a scanning animation. The left plot shows noise with a smoothness of 1.0 and the right plot shows the same random noise (equal seed) with smoothness of 1.3.

In [8]:
noise = Noise(smoothness=stack({"Default Noise": 1.0, "Smooth Noise": 1.3}, batch('c')))
grid = CenteredGrid(noise, x=64, y=64, z=64, bounds=Box(x=10, y=10, z=10))
plot(grid, animate='z', show_color_bar=False)
Out[8]:
Your browser does not support the video tag.

Solar System¶

This animation shows two planets circling the sun, using a PointCloud with spherical elements for visualization.

In [9]:
PLANETS = instance(planets='Sun,Earth,Mars')
x = tensor([(0, 0), (9, 0), (0, 12)], PLANETS, channel(vector='x,y'))
x = math.rotate_vector(x, math.linspace(0, wrap([0, 5, 3], PLANETS), batch(time=130)))
plot(PointCloud(Sphere(x, radius=wrap([1, .4, .2], PLANETS))), animate='time')
Out[9]:
Your browser does not support the video tag.

3D Voxels¶

Two spheres are placed in a $32^3$ domain, at positions (16, 16, 0) and (16, 16, 32). Their radii grow linearly in time. These spheres are then sampled on a regular grid and plotted as voxels. Additionally, we plot the cross section $y=16$ as a 2D heat map.

In [10]:
sphere = Sphere(x=16, y=16, z=0, radius=math.linspace(0, 16, batch(time=17)))
grid = CenteredGrid(union(sphere, sphere.shifted((0, 0, 32))), x=32, y=32, z=32)
plot({"3D": grid, "2D Slice": grid.y[16]}, animate='time', frame_time=300)
Out[10]:
Your browser does not support the video tag.

Spiral¶

For this animated spiral, we plot 200 points whose distance increases linearly from the origin and whose angle increases linearly from 0 to $\alpha = 20 \frac{t}{T}$ where $t$ denotes the current frame and $T$ the number of frames. When no geometric shape is specified, PointClouds are plotted as x.

Varying the parameters can produce vastly different patterns.

In [11]:
dst = math.linspace(0, 1, instance(points=200))
angle = math.linspace(0, math.linspace(PI*200, 1.1*PI*200, batch(t=200)), dst.shape)
plot(PointCloud(dst * vec(x=math.cos(angle), y=math.sin(angle))), animate='t')
Out[11]:
Your browser does not support the video tag.

Bouncing Balls¶

This demo visualizes the evolution of a PointCloud as a 3D scatter plot animation.

Thirty balls are placed at random locations. The initial velocities are sampled from a normal distribution with standard deviations $\sigma_x = \sigma_y = 1$ and $\sigma_z = 2$.

A simulation is than run for 100 frames, performing the following operations at each step:

  • Gravity is applied, $g_z = -9.81$,
  • Friction is computed proportional to velocity,
  • The balls are advected using Euler integration,
  • When below $z=0$, the y velocity is negated to simulate an elastic collision with the floor.
In [12]:
x0 = math.random_uniform(instance(balls=30), channel(vector='x,y,z')) + 5
balls = PointCloud(Sphere(x0, radius=.02), math.random_normal(x0.shape) * (1, 1, 2))
def step(balls, dt=.1):
  balls *= math.where(balls.points.vector['z'] < 0, (1, 1, -1), 1) * 0.7 ** dt
  return advect.points(balls, balls, dt) + (0, 0, -9.81 * dt)
plot(field.mask(iterate(step, batch(t=100), balls)), animate='t')
Out[12]:
Your browser does not support the video tag.

Burgers' Equation¶

Burgers' equation is a partial differential equation consisting of an advection term and a diffusion term acting on a vector field $v$ (velocity). It reads

$$\frac{\partial v}{\partial t} = \nu \frac{\partial^2 v}{\partial x^2} - v \frac{\partial v}{\partial x}.$$

Here, we simulate Burgers' equation on a $64^2$ grid for 100 time steps with $\Delta t = 0.5$, starting with a randomly generated initial condition. The evolution is plotted as a vector field. A standalone demo of Burgers' equation is also available here.

In [13]:
velocity = CenteredGrid(Noise(smoothness=1.5, vector='x,y'), extrapolation.PERIODIC, x=64, y=64) * 2
def burgers(v, dt=.5):
    return diffuse.explicit(advect.semi_lagrangian(v, v, dt), .08, dt)
vis.plot(iterate(burgers, batch(time=100), velocity), animate='time')
Out[13]:
Your browser does not support the video tag.

Incompressible Flow¶

Next, we simulate an incompressible fluid with moderate diffusion. We split the PDE

$$\frac{\partial v}{\partial t} = \nu \frac{\partial^2 v}{\partial x^2} - v \frac{\partial v}{\partial x} - \nabla p \quad \mathrm{s.t.} \quad \nabla \cdot v = 0$$

into advection, diffusion and pressure projection but will rely purely on numerical diffusion in this example. Starting from a random initial conditions, the fluid is simulated for 40 time steps and the vorticity $w = \nabla \times v$ and the pressure $p$ are shown. Also check out the tutorial notebook or the standalone Python scripts.

In [14]:
def incompressible_fluid_step(v: StaggeredGrid, p: CenteredGrid, dt=.5):
    return fluid.make_incompressible(advect.advect(v, v, dt), (), Solve('auto', 1e-5, 1e-5, x0=p))
trj = iterate(incompressible_fluid_step, batch(time=40), *fluid.make_incompressible(StaggeredGrid(Noise(), 0, x=64, y=64)))
plot({"Vorticity": field.curl(trj[0]), "Pressure": trj[1]}, animate='time', same_scale=False)
Out[14]:
Your browser does not support the video tag.

Pressure Solve¶

The incompressibility constraint $\nabla \cdot v$ in the Navier-Stokes equations is numerically achieved by solving the linear system of equations

$$\nabla p = \nabla \cdot v'$$

which yields the pseudo-pressure $p$. This is typically done with a conjugate gradient solver using a laplace stencil (5-point in 2D, 7-point in 3D). This demo visualizes how the pressure optimization progresses internally for a tentative velocity $v' = (1, 1)$ inside a circle at the center of the $100^2$ domain and $v' = 0$ outside.

In [15]:
with math.SolveTape(record_trajectories=True) as solves:
  fluid.make_incompressible(StaggeredGrid(Sphere(x=50, y=50, radius=20), 0, x=100, y=100))
plot(solves[0].x, animate='trajectory', frame_time=50)
Out[15]:
Your browser does not support the video tag.

Reaction-Diffusion¶

This simulation consists of two quantities $u$ and $v$ that interact via a non-linear partial differential equation (PDE) involving diffusion terms, $\nabla^2 u$ and $\nabla^2 v$. Depending on the exact form and parameters of the PDE, a myriad of resulting patterns can be achieved. The simulation is run for 1000 frames but we only plot every 10th since small time steps must be chosen for stability.

In [16]:
def reaction_diffusion(u, v, du=.19, dv=.05, f=.06, k=.062, dt=1.):
    return u + dt * du * field.laplace(u) - u * v**2 + f * (1 - u), v + dt * dv * field.laplace(v) + u * v**2 - (f + k) * v
trj_u, trj_v = iterate(reaction_diffusion, batch(time=1000), *[CenteredGrid(Noise(scale=20, smoothness=1.3), x=100, y=100) * .2 + .5]*2)
plot(trj_u.time[::10], animate='time')
Out[16]:
Your browser does not support the video tag.